package Dungeon;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;

import SquarePackage.Direction;
import SquarePackage.Rock;
import SquarePackage.Square;
import SquarePackage.Temperature;
import SquarePackage.TeleportableSquare;
import be.kuleuven.cs.som.annotate.*;

/**
 * Class for rooms. A room is a space in the dungeon, built with squares, which are all attached to eachother. 
 * 
 * @invar	A room contains at least one square.
 * 		  | getNumberOfBorders()>0
 * @invar	The temperature in all the squares is equal each other.
 * 		  | hasEverywhereCorrectTemperature()
 * @invar	The humidity in all the squares is equal each other.
 * 		  | hasEverywhereCorrectHumidity()
 * 
 * @author 	Brecht Reynders en Florian Hendrickx
 * @version	2.1
 */
 public class Room {
	/**
	 * Initializes a new room with a given dungeon, square and dimension.
	 * 
	 * @param 	dungeon
	 * 			The dungeon in which this room is located.
	 * @param 	square
	 * 			First square of this room.
	 * @param 	dimension
	 * 			Position of the square in the dungeon.
	 * @post	The given square is part of the new room.
	 * 		  	| new.contains(square) 
	 * @post	The given dimension is part of the new room.
	 * 		  	| new.contains(dimension) 
	 * @post	The given dungeon equals the new dungeon.
	 * 		  	| new.getDungeon() = dungeon
	 */
	public Room(Dungeon dungeon, Square square, Dimension dimension) throws IllegalArgumentException{
		positions = new HashMap<Dimension, Square>();
		positions.put(dimension, square);
		this.dungeon = dungeon;
		temperature = new Temperature(square.getTemp());
		humidity = square.getHumidity();
		externalRooms = new HashSet<Room>();
	}
	
	/**
	 * Initializes a new room with a given dungeon and hashmap.
	 * 
	 * @param 	dungeon
	 * 			The dungeon in which this room is located.
	 * @param 	positions
	 * 			Hashmap with one or more dimensions with squares. 
	 * @post	The given dungeon equals the new dungeon.
	 * 		  	| new.getDungeon() = dungeon
	 * @post	The given positions equal the positions.
	 * 		  	| new.getPositions() = positions
	 */
	public Room(Dungeon dungeon, HashMap<Dimension,Square> positions)throws IllegalArgumentException{
		this.dungeon = dungeon; 
		this.positions = positions;
		List<Square> list = new ArrayList<Square>(positions.values());
		this.temperature = new Temperature(list.get(0).getTemp());
		this.humidity = list.get(0).getHumidity();
		externalRooms = new HashSet<Room>();
	}
	
	/**
	 * Variable registering all the dimensions and Squares in this room.
	 */
	private HashMap<Dimension, Square> positions;
	
	/**
	 * Variable registering the dungeon in which the room is located.
	 */
	private final Dungeon dungeon;
	
	/**
	 * Variable registering all the rooms which are also part of this room but in other dungeons.
	 */
	private Set<Room> externalRooms;
	
	/**
	 * Variable registering whether this room is terminated.
	 */
	private boolean isTerminated = false;
	
	/**
	 * Variable registering the temperature in this room.
	 */
	private Temperature temperature;
	
	/**
	 * Variable registering the humidity in this room.
	 */
	private BigDecimal humidity;
		
	/**
	 * Return the dungeon in which this room is located.
	 */
	@Basic
	public final Dungeon getDungeon(){
		return dungeon;
	}
	
	/**
	 * Return whether this room is terminated or not.
	 */
	@Basic @Raw
	public boolean isTerminated(){
		return isTerminated;
	}
		
	/**
	 * Return the humidity of this square.
	 */
	@Basic @Raw
	public BigDecimal getHumidity(){
		return humidity;
	}
	
	/**
	 * Return the temperature of this square.
	 */
	@Basic @Raw
	public Temperature getTemperature(){
		return temperature;
	}
	
	/**
	 * Return an hashmap with all the dimensions and squares of this room.
	 */
	@Basic @Raw
	public HashMap<Dimension, Square> getPositions(){
		return (HashMap<Dimension, Square>)positions.clone();
	}
	
	/**
	 * Return a collection of all the squares in this room .
	 */
	@Raw
	public List<Square> getAllSquares(){
		List<Square> list = new ArrayList<Square>(getPositions().values());
		for(Room room: getExternalRooms()){
			list.addAll(room.getSquaresOfThisDungeon());
		}
		return list;
	}
	
	/**
	 * Returns a collection of all the squares in the dungeon of this room.
	 */
	public List<Square> getSquaresOfThisDungeon(){
		return new ArrayList<Square>(getPositions().values());
	}
	
	/**
	 * Return a list of all the dimensions in this room.
	 */
	@Raw
	public Collection<Dimension> getAllDimensions(){
		return getPositions().keySet();
	}
	
	/**
	 * Return the number of squares in this room.
	 */
	public int getNumberOfSquares(){
		return getAllSquares().size();
	}
	
	/**
	 * Return the square on the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension of which the square must be returned.
	 * @return	The square at the given dimension.
	 * 		  	| positions.get(dimension)
	 */
	public Square getSquareAt(Dimension dimension){
		return positions.get(dimension);
	}
	
	/**
	 * Returns a set of all the rooms outside this dungeon.
	 */
	public Set<Room> getExternalRooms(){
		return externalRooms;
	}

	/**
	 * Checks whether this room contains the given square.
	 * 
	 * @param	square
	 * 			The square to search in this room.
	 * @return 	True if and only if the given square is part of this room.
	 * 			| result = getPositions.containsValue(square)
	 */
	public boolean contains(Square square){
		return getPositions().containsValue(square);
	}
	
	/**
	 * Check whether this room contains the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension to search in this square.
	 * @return	True if and only if the given dimension is part of this room.
	 * 			| result = getPositions.containsKey(dimension)
	 */
	public boolean contains(Dimension dimension){
		return getPositions().containsKey(dimension);
	}

	/**
	 * Remove a square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension on which the square must be removed.
	 * @post	The square isn't a square of this room anymore.
	 * 		  | new.getSquareAt(dimension) = null
	 * @effect	The square is terminated.
	 * 		  | this.getSquareAt(dimension).isTerminated()
	 */
	public void removeSquareAt(Dimension dimension){
		getSquareAt(dimension).terminate();
		positions.remove(dimension);
	}
	
	/**
	 * Remove a square at the given dimension, because it is already used in another room.
	 * 	
	 * @param 	dimension
	 * 			Dimension of the double square.
	 * @post	The square isn't a square of this room anymore.
	 * 		  	| new.getSquareAt(dimension) = null
	 */
	public void removeDoubleSquareAt(Dimension dimension){
		positions.remove(dimension);
	}
//	
//	/**
//	 * Returns the hashcode of this room.
//	 */
//	@Override
//	public int hashCode(){
//		return (int) (31  );
//	}
//	
//	/**
//	 * Check whether this room equals the given room.
//	 * 
//	 * @param	room
//	 * 			The room to check whether it is equal or not.
//	 * @return	True if and only if the given room equals this room.
//	 */
//	@Override
//	public boolean equals(Object room){
//		if(room == null)
//			return false;
//		if(room.getClass()!= Room.class)
//			return false;
//		if(!getPositions().equals(((Room) room).getPositions()))
//			return false;
//		if(!getDungeon().equals(((Room)room).getDungeon()))
//			return false;
//		return true;
//	}
	
	/**
	 * Merge this room with the given room.
	 * 
	 * @param 	room
	 * 			The room with which this room should merge.
	 * @post	This square contains the squares of both of the rooms.
	 * 		  	| this.getAllSquares() + room.getAllSquares()= new.getAllSquares()
	 * @effect	The given room is empty and gets removed.
	 * 		  	| !new.getRooms().contains(room)
	 * @effect	The temperature and humidity are equal in all the squares of this room.
	 * 		  	| updateTemperatureAndHumidity()
	 */
	public void mergeWith(Room room){
		if(!room.getDungeon().equals(getDungeon()))
			throw new IllegalArgumentException("This room is not in the same dungeon.");
		positions.putAll(room.getPositions());
		externalRooms.addAll(room.getExternalRooms());
		changeRoom(room);
		getDungeon().removeDoubleRoom(room, this);
		updateTemperatureAndHumidity();
	}
	
	/**
	 * Merge this room with a given room from another dungeon.
	 * 
	 * @param 	room
	 * 			The room from another dungeon.
	 */
	public void externalMergeWith(Room room){
		if(!room.getDungeon().equals(getDungeon())){
			externalRooms.add(room);
			externalRooms.addAll(room.getExternalRooms());
			room.externalMergeWith(this);
			updateTemperatureAndHumidity();
		}
		else{
			throw new IllegalArgumentException("This room is not in another dungeon.");
		}
	}
	
	private void changeRoom(Room room){
		for(Room externalRoom: room.getExternalRooms()){
			externalRoom.getExternalRooms().remove(room);
			externalRoom.externalMergeWith(this);
		}
	}
	
	/**
	 * Update the temperature and humidity in this room, so that the humidity and the temperature in all the squares are equal.
	 * 
	 * @post	The temperature in all the squares equal each other.
	 * 		  	| hasEverywhereCorrectTemperature()
	 * @post	The humidity in all the squares equal each other.
	 * 		  	| hasEverywhereCorrectHumidity()
	 */
//TODO UPDATE ROCKS IN SAME DUNGEON
	public void updateTemperatureAndHumidity(){
		if(!hasEverywhereCorrectHumidity()){
			BigDecimal newHumidity = new BigDecimal("0.00");
			for(Square square: getAllSquares()){
				newHumidity = newHumidity.add(square.getHumidity());
			}
			BigDecimal divisor = new BigDecimal(getNumberOfSquares());
			newHumidity = newHumidity.divide(divisor,2,RoundingMode.HALF_UP);
			for(Square square: getAllSquares()){
				((TeleportableSquare) square).setHumidity(newHumidity);
			}
			this.humidity = newHumidity;
		}
		if(!hasEverywhereCorrectTemperature()){
			double newTemperature = 0.0;
			for(Square square: getAllSquares()){
				newTemperature += square.getTemp();
			}
			newTemperature = newTemperature/getNumberOfSquares();
			for(Square square: getAllSquares()){
				((TeleportableSquare)square).setTemperature(newTemperature);
			}
			this.temperature.setTemperature(newTemperature);
		}
		if(getAllSquares().get(0) instanceof Rock){
			((Rock)getAllSquares().get(0)).updateTemperature();
			this.temperature = new Temperature(getAllSquares().get(0).getTemp());
		}
	}

	
	/**
	 * Checks whether this room has  everywhere the correct temperature.
	 * 
	 * @return 	True if and only if the temperature in all the squares equal each other.
	 */
	public boolean hasEverywhereCorrectTemperature(){
		for(Square square: getPositions().values()){
			if(square.getTemp()!=getAllSquares().get(0).getTemp())
				return false;
		}
		return true;
	}
	
	/**
	 * Returns a map of dimensions and squares which are in the same room.
	 * 
	 * @param 	dimension
	 * 			The dimension of a square which is certainly in the room to return.
	 * @return	A map of dimensions and squares which are all in the same room.
	 * 		  	| result = getNeighbourSquares(dimension, null)
	 */
	public Map<Dimension, Square> getNeighbourSquares(Dimension dimension){
		return getNeighbourSquares(dimension,null);
	}
	
	/**
	 * Returns a map of dimensions and squares which are in the same room.
	 * 
	 * @param 	dimension
	 * 			The dimension of a square which is certainly in the room to return.
	 * @param 	neighbourSquares
	 * 			The dimensions and squares which are already found.
	 * @return	A map of dimensions and squares which are all in the same room.
	 */
	public Map<Dimension, Square> getNeighbourSquares(Dimension dimension, Map<Dimension, Square> neighbourSquares){
		if(neighbourSquares==null){
			neighbourSquares = new HashMap<Dimension,Square>();
		}
		neighbourSquares.put(dimension, getPositions().get(dimension));
		for(Dimension other: getPositions().keySet()){
			if(isValidNeighbourSquare(dimension, other, neighbourSquares)){
				getNeighbourSquares(other, neighbourSquares);
		 	}
		}
		return neighbourSquares;
	}
	
	/**
	 * Checks whether the other dimension is a valid dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension to find the neighbours.
	 * @param 	other
	 * 			The second dimension to verify if it's a valid neighbour.
	 * @param 	neighbourSquares
	 * 			The dimensions and squares that are already found.
	 * @return	True if and only if the dimensions are neighbours and the dimension isn't a key already and the borders between the dimensions are open.
	 * 		  	| return = dimension.isNeighbour(other) && !neighbourSquares.containsKey(other) && !getSquareAt(dimension).getBorderAt(dimension.getNeighbourDirection(other)).getIsolation()
	 * @note	If the return is false, it doesn't mean that the other dimension is not a valid dimension.
	 */
	private boolean isValidNeighbourSquare(Dimension dimension, Dimension other, Map<Dimension, Square> neighbourSquares){
		if(!( dimension.isNeigbour(other) && !neighbourSquares.containsKey(other)))
				return false;
		return !getSquareAt(dimension).getBorderAt(dimension.getNeigbourDirection(other)).getIsolation();
	}
	
	/**
	 * Checks whether this room has everywhere the same humidity.
	 * 
	 * @return	True if and only if all the squares have the same humidity.
	 * 
	 */
	public boolean hasEverywhereCorrectHumidity(){
		if(getPositions().size()==1){
			return true;
		}
		for(Square square: getPositions().values()){
			if(!square.getHumidity().equals(getAllSquares().get(0).getHumidity()))
				return false;
		}
		return true;
	}
	
	/**
	 * Terminates this room.
	 */
	public void terminate(){
		for(Dimension dimension : getAllDimensions()){
			removeSquareAt(dimension);
		}
		isTerminated = true;
	}

	/**
	 * Returns a neighbouring dimension of this room.
	 * 
	 * @param 	dimension
	 * 			The dimension of which a neighbouring dimension is needed.
	 * @return	A neighbouring dimension or null, if the room contains just one square.
	 * 		  	| result = dimension(if(getPositions().containsKey(dimension))) else(null)
	 */
	public Dimension getNeighbourPosition(Dimension dimension){
		for(Direction direction: Direction.values()){
			if(getPositions().containsKey(dimension.getDimensionIn(direction)))
				return dimension.getDimensionIn(direction);
		}
		return null;
	}
	
	/**
	 * Removes the squares that are transferred to the given room.
	 * 
	 * @param 	room
	 * 			Room that contains some squares which are also in this room.
	 * @post	This room and the given room don't contain the same squares anymore.
	 */
	public void removeDoubleSquares(Room room){
		for(Dimension dimension: getAllDimensions()){
			room.removeDoubleAt(dimension);
		}
	}
	
	/**
	 * Removes a double square at the given dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension on which there is a double room.
	 * @post	The square at the given dimension isn't part of 2 rooms anymore.
	 */
	public void removeDoubleAt(Dimension dimension){
		positions.remove(dimension);
	}
	/**
	 * Returns the dimension of the given square.
	 * 
	 * @param 	square
	 * 			The square of which we want to know the dimension.
	 * @return	The dimension of the square.
	 * 		  	| result = dimension(if(getPositions().get(dimension).equals(square)))
	 * 		  	| 			else(null)
	 */
	public Dimension getDimensionOf(Square square){
		for(Dimension dimension: getPositions().keySet()){
			if(getPositions().get(dimension).equals(square))
				return dimension;
		}
		return null;
	}
	/**
	 * Return the humidity in percentage form
	 * 
	 * @return	humidity followed by a percentage sign "%"
	 */
	@Raw
	public String getHumidityPercentage(){
		NumberFormat myFormatter = new DecimalFormat("##,##");
		myFormatter.setMinimumFractionDigits(2);
		myFormatter.setMaximumIntegerDigits(3);
		DecimalFormatSymbols commaToPointChanger = new DecimalFormatSymbols(); 
		commaToPointChanger.setDecimalSeparator('.');
		((DecimalFormat) myFormatter).setDecimalFormatSymbols(commaToPointChanger);
		String output = "" + myFormatter.format(getHumidity()) + "%";
		return output;
	}
}
